% Authors: Francisco Pinto & Bruno Maia
% Date: 22-09-2011
% This file is part of "Breakthrough"

% Game logic

?- use_module(library(random)).
?- use_module(library(system)).

?- ensure_loaded('ui.pro').
?- ensure_loaded('constants.pro').
?- ensure_loaded('utils.pro').
?- ensure_loaded('persistence.pro').
?- ensure_loaded('ai.pro').

?- dynamic first/1.
?- dynamic player_type/2.

% initial_board/1
% modes: (+), (-)
% true if parameter Board is the game's initial board
initial_board(Board) :-
  player1_representation(Player1),
  player2_representation(Player2),
  blank_representation(Blank),
  
  board_size(BoardWidth),
  generate_list(Player1, BoardWidth, Player1Row),
  generate_list(Player2, BoardWidth, Player2Row),
  generate_list(Blank, BoardWidth, BlankRow),
  
  home_rows(NHomeRows),
  blank_rows(NBlankRows),
  generate_list(Player1Row, NHomeRows, Player1Home),
  generate_list(Player2Row, NHomeRows, Player2Home),
  generate_list(BlankRow, NBlankRows, BlankRows),
  
  concatenate(Player1Home, BlankRows, TempBoard),
  concatenate(TempBoard, Player2Home, Board).

% valid_coordinates/2
% modes: (+,+)
% true if X and Y correspond to valid cartesian coordinates within the board
valid_coordinates(X, Y) :-
  board_size(BoardSize),
  X >= 0,
  X < BoardSize,
  Y >= 0,
  Y < BoardSize.

% get_board_element/4
% modes: (+,+,+,?)
% true if cartesian coordinates (X, Y) of Board correspond to Element
get_board_element(Board, X, Y, Element) :-
  valid_coordinates(X, Y),
  get_element(Board, Y, Row),
  get_element(Row, X, Element).
  

% valid_movement/6
% modes: (+,+,+,?,?,+)
% true if (XFinal,YFinal) correspond to valid cartesian coordinates 
% for a movement from position (XPiece, YPiece), where a piece
% owned by Player should exist in Board 
valid_movement(Board, XPiece, YPiece, XPiece, YFinal, Player) :-
  valid_coordinates(XPiece, YPiece),
  get_movement_y_for_player(Player, YPiece, YFinal),
  get_board_element(Board, XPiece, YFinal, Element),
  blank_representation(B),
  Element = B.

valid_movement(Board, XPiece, YPiece, XFinal, YFinal, Player) :-
  valid_coordinates(XPiece, YPiece),
  get_movement_y_for_player(Player, YPiece, YFinal),
  XFinal is XPiece - 1,
  get_board_element(Board, XFinal, YFinal, Element),
  \+Element = Player.

valid_movement(Board, XPiece, YPiece, XFinal, YFinal, Player) :-
  valid_coordinates(XPiece, YPiece),
  get_movement_y_for_player(Player, YPiece, YFinal),
  XFinal is XPiece + 1,
  get_board_element(Board, XFinal, YFinal, Element),
  \+Element = Player.

% diagonal/2
% modes: (+,+)
% true if XPiece is different from XFinal (i.e. the movement is diagonal)
diagonal(XPiece, XFinal) :-
  XPiece \= XFinal.

% get_movement_y_for_player/3
% modes: (+,+,-), (+,-,+) 
% true if YDesired is a valid y-axis coordinate for a 
% movement from YPiece, where there is a piece owned by Player
get_movement_y_for_player(p1, YPiece, YDesired) :-
  YDesired is YPiece + 1.

get_movement_y_for_player(p2, YPiece, YDesired) :-
  YDesired is YPiece - 1.

% element_present_in_row/3
% Modes: (+,?,?)
% True if Element is present at RowIndex in Board
element_present_in_row(Board, RowIndex, Element) :-
  get_element(Board, RowIndex, Row),
  member(Element, Row).

% winner/2
% Modes: (+,?)
% True if Board is a winning board for Player
winner(Board, Player) :-
  player2_representation(Player),
  element_present_in_row(Board, 0, Player).

winner(Board, Player) :-
  player1_representation(Player),
  board_size(BoardSize),
  BoardMaxIndex is BoardSize - 1,
  element_present_in_row(Board, BoardMaxIndex, Player).

winner(Board, Player) :-
  opposing_player(Player, OpposingPlayer),
  \+(find_piece(Board, OpposingPlayer, 0, 0, _, _)).

% move/7
% Modes: (+,+,+,+,+,+,?)
% True if NewBoard is the resulting board if the piece belonging to Player
% at (XPiece, YPiece) gets moved to (XFinal, YFinal) in Board
move(Board, XPiece, YPiece, XFinal, YFinal, Player, NewBoard) :-
  get_element(Board, YFinal, TempRowFinal),
  set_element_at_index(TempRowFinal, XFinal, Player, NewRowFinal),
  set_element_at_index(Board, YFinal, NewRowFinal, TempBoard),
  
  blank_representation(Blank),
  get_element(TempBoard, YPiece, TempRowPiece),
  set_element_at_index(TempRowPiece, XPiece, Blank, NewRowPiece),
  set_element_at_index(TempBoard, YPiece, NewRowPiece, NewBoard).

calculate_depth(TotalMoveCount, 2) :-
  TotalMoveCount > 30.

calculate_depth(TotalMoveCount, 3) :-
  TotalMoveCount > 20.

calculate_depth(TotalMoveCount, 4) :-
  TotalMoveCount > 10.

calcultate_depth(TotalMoveCount, 5) :-
  TotalMoveCount > 5.

calcultate_depth(TotalMoveCount, 6) :-
  TotalMoveCount > 0.

% adapt_depth/3
% Modes: (+,+,?)
% True if the computed depth for game situation Board, Player
% is Depth
adapt_depth(Player, Board, Depth) :-
  player1_representation(Player1),
  moves(Board, Player1, Player1Moves),
  player2_representation(Player2),
  moves(Board, Player2, Player2Moves),
  list_length(Player1Moves, Player1MovesTotal),
  list_length(Player2Moves, Player2MovesTotal),
  TotalMoveCount is Player1MovesTotal + Player2MovesTotal,
  calculate_depth(TotalMoveCount, SuggestedDepth),
  player_difficulty(Player, Difficulty),
  DifficultyWeightedDepth is SuggestedDepth - (3 - Difficulty),
  (DifficultyWeightedDepth > 0 
    -> Depth = DifficultyWeightedDepth
    ;  Depth = 1).

% adapt_depth/3
% Modes: (+,+,?)
% True if the computed depth for game situation Board, Player
% is Depth
game_loop(Board, Player, Winner) :-
  print_board(Board),
  player_type(Player, PlayerType),
  query_movement(Board, Player, PlayerType, NewBoard),
  (winner(NewBoard, Player) 
    -> Winner = Player, print_board(NewBoard)
    ; opposing_player(Player, OpposingPlayer), game_loop(NewBoard, OpposingPlayer, Winner)
  ).

% run/0
% Modes: N/A
% The game's entry point
run :-
  now(T),
  X is (T mod 30269) + 1,
  Y is (T mod 30307) + 1,
  Z is (T mod 30323) + 1,
  B is T,
  setrand(random(X,Y,Z,B)),
  write('+--------------------------+'), nl,
  write('| Welcome to Breakthrough! |'), nl,
  write('|        ----------        |'), nl,
  write('| Original game by         |'), nl,
  write('|               Dan Troyka |'), nl,
  write('|        ----------        |'), nl,
  write('| Implementation by        |'), nl,
  write('|               Bruno Maia |'), nl,
  write('|      maia.bruno@fe.up.pt |'), nl,
  write('|                          |'), nl,
  write('|          Francisco Pinto |'), nl,
  write('| francisco.pinto@fe.up.pt |'), nl,
  write('+--------------------------+'), nl,
  main_menu(Board, Player),
  player1_representation(Player1),
  player2_representation(Player2),
  player(1, Player1Type),
  (Player1Type = 'C' -> query_difficulty(Player1) ; true),
  player(2, Player2Type),
  (Player2Type = 'C' -> query_difficulty(Player2) ; true),
  assert(player_type(Player1, Player1Type)),
  assert(player_type(Player2, Player2Type)),
  game_loop(Board, Player, Winner),
  print_winner(Winner),
  retract(player_type(Player1, Player1Type)),
  retract(player_type(Player2, Player2Type)).

% find_piece/6
% Modes: (+,?,?,?,?,?)
% True if, in Board, the piece at XPiece, YPiece belongs to Player
find_piece([], _, _, _, _, _) :- fail.

find_piece([[Player|_]|_], Player, XPiece, YPiece, XPiece, YPiece).

find_piece([[]|Row], Player, _, YCoord, XPiece, YPiece) :-
  NewYCoord is YCoord + 1,
  find_piece(Row, Player, 0, NewYCoord, XPiece, YPiece).

find_piece([[_|Column]|Row], Player, XCoord, YCoord, XPiece, YPiece) :-
  NewXCoord is XCoord + 1,
  find_piece([Column|Row], Player, NewXCoord, YCoord, XPiece, YPiece).

% query_movement/4
% Modes: (+,+,+,-)
% Queries the player for a move to be made. Resulting board is placed in NewBoard
query_movement(Board, Player, 'H', NewBoard) :-
  movement_menu(Board, Player),
  query_position('Initial position', XInitial, YInitial),
  (get_board_element(Board, XInitial, YInitial, Player) 
  -> query_position('Final position', XFinal, YFinal),
     (  valid_movement(Board, XInitial, YInitial, XFinal, YFinal, Player)
     -> move(Board, XInitial, YInitial, XFinal, YFinal, Player, NewBoard)
     ;  write('Invalid movement!'), nl,
        query_movement(Board, Player, 'H', NewBoard)
     )
  ;  write('Invalid coordinates!'), nl,
     query_movement(Board, Player, 'H', NewBoard)
  ).

query_movement(Board, Player, 'C', NewBoard) :-
  player_text(Player, Text),
  format('~w\'s turn~n',[Text]),
  assert(first(Player)),
  adapt_depth(Player, Board, Depth),
  minimax(Player, Board, NewBoard, Depth),
  retract(first(Player)).

% winners/3
% Modes: (+,?,?)
% True if at least one of Player or OpposingPlayer can be declared winners from
% Board
winners(Board, Player, _) :-
  winner(Board, Player).

winners(Board, _, OpposingPlayer) :-
  winner(Board, OpposingPlayer).

% moves/3
% Modes: (+,?,?)
% True if BoardList corresponds to all possible boards derived 
% from moves Player can make in Board
moves(Board, Player, BoardList) :-
  opposing_player(Player, Opposing),
  \+winners(Board, Player, Opposing),
  findall(NewBoard,
          (
            find_piece(Board, Player, 0, 0, XPiece, YPiece),
            valid_movement(Board, XPiece, YPiece, XFinal, YFinal, Player),
            move(Board, XPiece, YPiece, XFinal, YFinal, Player, NewBoard)
          ),
          BoardList).

% moves/3
% Modes: (+,+,?)
% True if Val is Board's value for Player
static_evaluate_board(Board, Player, Val) :-
  winner(Board, Player),
  evaluate_board(Board, Player, BoardVal),
  Val is (1000 + BoardVal).

static_evaluate_board(Board, Player, Val) :-
  opposing_player(Player, Opposing),
  winner(Board, Opposing),
  evaluate_board(Board, Player, BoardVal),
  Val is -(1000 + BoardVal). 

% evaluate_pieces/3
% Modes: (+,+,?)
% True if Val is Player's PieceList value
evaluate_pieces([], _, 0).

evaluate_pieces([[_, YPiece]|Pieces], Player1, Val) :-
  player1_representation(Player1),
  evaluate_pieces(Pieces, Player1, TailVal),
  Val is TailVal + exp(YPiece,2)/2 + 1.

evaluate_pieces([[_, YPiece]|Pieces], Player2, Val) :-
  player2_representation(Player2),
  evaluate_pieces(Pieces, Player2, TailVal),
  board_size(MaxIndex),
  YVal is YPiece-MaxIndex,
  Val is TailVal + (exp((YVal),2))/2 + 1.

% evaluate_board/3
% Modes: (+,+,?)
% True if Val is Board's value for Player
evaluate_board(Board, Player, Val) :-
  opposing_player(Player, OpposingPlayer),
  findall([XPieceP1, YPieceP1], find_piece(Board, Player, 0, 0, XPieceP1, YPieceP1), PlayerPieces),
  findall([XPieceP2, YPieceP2], find_piece(Board, OpposingPlayer, 0, 0, XPieceP2, YPieceP2), OpposingPlayerPieces),

  evaluate_pieces(PlayerPieces, Player, PlayerPiecesValue),
  
  evaluate_pieces(OpposingPlayerPieces, OpposingPlayer, OpposingPlayerPiecesValue),
  
  Val is PlayerPiecesValue - OpposingPlayerPiecesValue.
